/**
 * 通用掉落工厂
 *
 * 使用方法示例：
 * import DropFactory from '../model/DropFactory.js'
 * const r1 = await DropFactory.drop('副本', 10)
 * const r2 = await DropFactory.drop('秘境', 9, { seed: 12345, unique: true, includeDebug: true })
 * 
 * 参数：
 * - source: '副本' | '秘境'（默认 '副本'）
 * - layer: number 层级（1–20）
 * - opts:
 *   - seed?: number 随机种子（可复现）
 *   - unique?: boolean 同一次掉落去重（默认 false）
 *   - perCategoryLimit?: { [类别: string]: number } 每类别上限（可选）
 *   - includeDebug?: boolean 返回时包含调试信息（权重、分配）
 * 
 * 返回：
 * {
 *   来源: '副本' | '秘境',
 *   层级: number,
 *   掉落: Array<{ 类别, 套装名称?, 元素?, 星级?, 稀有度?, id, 名称, 部位?, 来源 }>,
 *   debug?: { total, weights, alloc, layerName }
 * }
 */
import lodash from 'lodash'
import Data from '../components/Data.js'
import ActionLog from './ActionLogFactory.js'
import ArtifactFactory, { generateArtifactFromBase } from './ArtifactFactory.js'

const ROOT = ''

function read(path){
  return Data.readJSON(path, ROOT)
}
function readList(path){
  const d = Data.readJSON(path, ROOT)
  return Array.isArray(d) ? d : []
}
function safeArray(x){
  return Array.isArray(x) ? x : []
}
function nonNeg(x){
  const n = Number(x)
  return Number.isFinite(n) && n > 0 ? n : 0
}
function clamp01(x){
  const n = Number(x)
  if (!Number.isFinite(n)) return 0
  return Math.max(0, Math.min(1, n))
}
function validateSource(src){
  return src === '副本' || src === '秘境' ? src : '副本'
}
function validateLayer(layer){
  const n = parseInt(layer)
  if (!Number.isFinite(n)) return 1
  return Math.max(1, Math.min(20, n))
}

function flattenEquip(equipSets, sourceKey){
  const ret = []
  for (const set of equipSets){
    const name = set['套装名称']
    const parts = safeArray(set['部件'])
    for (const p of parts){
      const src = safeArray(p['掉落来源'])
      const prob = p['掉落概率'] || {}
      ret.push({
        类别:'装备',
        套装名称:name,
        id:p.id,
        名称:p['名称'],
        部位:p['部位'],
        稀有度:set['稀有度'],
        星级:set['星级'],
        是否副本掉落: p['是否副本掉落'],
        掉落来源:src,
        概率:prob,
        键:sourceKey
      })
    }
  }
  return ret
}

function flattenElixir(items){
  return items.map(i=>({
    类别:'丹药',
    id:i.id,
    名称:i['名称'],
    稀有度:i['稀有度'],
    类型:i['类型'],
    品阶:i['品阶'],
    是否副本掉落: i['是否副本掉落'],
    掉落来源:i['掉落来源']||[],
    概率:i['掉落概率']||{},
    键:'通用'
  }))
}

function flattenMaterial(items){
  return items.map(i=>({
    类别:'材料',
    id:i.id,
    名称:i['名称'],
    稀有度:i['稀有度'],
    类型:i['类型'],
    品级:i['品级'],
    是否副本掉落: i['是否副本掉落'],
    掉落来源:i['掉落来源']||[],
    概率:i['掉落概率']||{},
    键:'通用'
  }))
}

function flattenArtifact(sets){
  const ret=[]
  for (const set of sets){
    const sname = set['套装名称']
    const parts = set['部件']||[]
    for (const p of parts){
      ret.push({
        类别:'圣遗物',
        套装名称:sname,
        元素:set['元素'],
        星级:set['星级'],
        稀有度:set['稀有度'],
        id:p.id,
        名称:p['名称'],
        部位:p['部位'],
        是否副本掉落: p['是否副本掉落'],
        掉落来源:p['掉落来源']||[],
        概率:p['掉落概率']||{},
        键:'副本'
      })
    }
  }
  return ret
}

function getLayerName(source, layer){
  const num = lodash.isNumber(layer)?layer:parseInt(layer)
  if (source==='副本') return `副本${num}层`
  return `秘境${num}层`
}

function findPoolCoeff(source, layerName){
  const pool = read('resources/data/Drop/副本掉落池.json')
  const item = (pool['层级']||[]).find(x=>x['名称']===layerName)
  const globalTimes = pool['全局']||{}
  return {
    层级系数: item?item['层级系数']||1:1,
    类别权重: item?item['类别权重']||{}:{},
    稀有度加成: item?item['稀有度加成系数']||{}:{},
    基础掉落次数: globalTimes['基础掉落次数']||1,
    最大掉落次数: globalTimes['最大掉落次数']||3
  }
}

function getDungeonCfg(source, layer){
  if (source==='副本'){
    const d = readList('resources/data/Dungeon/副本.json')
    const cfg = d.find(x=>x['层级']===layer)
    return cfg||{}
  }
  const d = readList('resources/data/Dungeon/秘境.json')
  const cfg = d.find(x=>x['层级']===layer)
  return cfg||{}
}

function filterBySource(items, layerName, source){
  const altLayerName = source==='副本' ? layerName.replace(/^副本/, '秘境') : layerName.replace(/^秘境/, '副本')
  return items.filter(i=>{
    const src = safeArray(i['掉落来源'])
    return src.includes(layerName) || src.includes(altLayerName)
  })
}

function applyWhitelist(items, wl){
  return items.filter(i=>{
    if (i.类别==='圣遗物' && wl['圣遗物']){
      return wl['圣遗物'].includes(i['套装名称'])
    }
    if (i.类别==='装备' && wl['装备套装']){
      return wl['装备套装'].includes(i['套装名称'])
    }
    if (i.类别==='材料' && wl['材料']){
      return wl['材料'].includes(i.id)
    }
    if (i.类别==='丹药' && wl['丹药类型']){
      return wl['丹药类型'].includes(i['类型'])
    }
    return true
  })
}

function filterByRange(items, cfg){
  const ret=[]
  const ed = cfg['可掉落-装备']||{}
  const dl = cfg['可掉落-丹药']||{}
  const ml = cfg['可掉落-材料']||{}
  for (const i of items){
    if (i.类别==='装备'){
      const okStar = ed['星级范围']? (i['星级']>=ed['星级范围'][0] && i['星级']<=ed['星级范围'][1]):true
      const okSet = ed['套装']? ed['套装'].includes(i['套装名称']):true
      if (okStar && okSet) ret.push(i)
    } else if (i.类别==='丹药'){
      const okRank = dl['品阶范围']? (i['品阶']>=dl['品阶范围'][0] && i['品阶']<=dl['品阶范围'][1]):true
      const okRare = dl['稀有度']? dl['稀有度'].includes(i['稀有度']):true
      const okType = dl['类型']? dl['类型'].includes(i['类型']):true
      if (okRank && okRare && okType) ret.push(i)
    } else if (i.类别==='材料'){
      const okRank = ml['品级范围']? (i['品级']>=ml['品级范围'][0] && i['品级']<=ml['品级范围'][1]):true
      const okRare = ml['稀有度']? ml['稀有度'].includes(i['稀有度']):true
      if (okRank && okRare) ret.push(i)
    } else {
      ret.push(i)
    }
  }
  return ret
}

function weightOf(item, layerName, coeff, source){
  let base = lodash.get(item,'概率.'+layerName,0) || 0
  if (!base){
    const alt = source==='副本' ? layerName.replace(/^副本/,'秘境') : layerName.replace(/^秘境/,'副本')
    base = lodash.get(item,'概率.'+alt,0) || 0
  }
  if (!base){
    const probs = Object.values(item['概率']||{})
    base = probs.length ? Math.max(...probs.map(x=>Number(x)||0)) : 0
  }
  const rare = (coeff.稀有度加成 && coeff.稀有度加成[item['稀有度']]) || 1
  return base * coeff.层级系数 * rare
}

function createPRNG(seed){
  if (!Number.isFinite(seed)) return Math.random
  let t = seed >>> 0
  return function(){
    t += 0x6D2B79F5
    let r = Math.imul(t ^ t >>> 15, 1 | t)
    r ^= r + Math.imul(r ^ r >>> 7, 61 | r)
    return ((r ^ r >>> 14) >>> 0) / 4294967296
  }
}

function pickWeighted(cands, count, rng){
  const res=[]
  let weights = cands.map(c=>Math.max(0,c.w))
  let sum = weights.reduce((a,b)=>a+b,0)
  for (let i=0;i<count;i++){
    if (sum<=0 || cands.length===0) break
    const r = (rng||Math.random)()*sum
    let acc=0
    for (let j=0;j<cands.length;j++){
      acc+=weights[j]
      if (r<=acc){
        res.push(cands[j].i)
        break
      }
    }
  }
  return res
}

function allocCounts(total, weights){
  const cats = Object.keys(weights)
  const arr = cats.map(k=>({k,v:weights[k]}))
  const sum = arr.reduce((a,b)=>a+b.v,0)
  const ret = {}
  if (sum<=0){
    for (const it of arr){ ret[it.k]=0 }
    return ret
  }
  const fracs = arr.map(it=>({k:it.k, raw: total * (it.v/sum)}))
  let assigned = 0
  // 先取地板
  for (const f of fracs){
    const cnt = Math.floor(f.raw)
    ret[f.k] = cnt
    assigned += cnt
    f.rem = f.raw - cnt
  }
  // 分配余数
  let remain = Math.max(0, total - assigned)
  fracs.sort((a,b)=>b.rem - a.rem)
  let idx = 0
  while (remain>0 && fracs.length>0){
    ret[fracs[idx].k] = (ret[fracs[idx].k]||0) + 1
    remain--
    idx = (idx+1) % fracs.length
  }
  return ret
}

async function drop(source='副本', layer=1, opts={}){
  source = validateSource(source)
  layer = validateLayer(layer)
  const layerName = getLayerName(source, layer)
  const coeff = findPoolCoeff(source, layerName)
  const dungeon = getDungeonCfg(source, layer)
  const baseTimes = lodash.get(dungeon,'掉落次数.基础', coeff.基础掉落次数)
  const maxTimes = lodash.get(dungeon,'掉落次数.最大', coeff.最大掉落次数)
  const total = Math.min(Math.max(0, baseTimes), Math.max(0, maxTimes))
  const weightsRaw = dungeon['类别权重']||coeff.类别权重||{}
  const weights = {}
  for (const k of ['丹药','装备','材料','圣遗物']){
    weights[k] = clamp01(weightsRaw[k]||0)
  }

  let equipSets = []
  let elixirs = []
  let mats = []
  let arts = []
  try { equipSets = readList('resources/data/Equip/装备.json') } catch {}
  try { elixirs = readList('resources/data/Elixir/丹药.json') } catch {}
  try { mats = readList('resources/data/Material/材料.json') } catch {}
  try { arts = readList('resources/data/Artifact/圣遗物.json') } catch {}

  let pool = []
  pool = pool.concat(flattenEquip(equipSets, source))
  pool = pool.concat(flattenElixir(elixirs))
  pool = pool.concat(flattenMaterial(mats))
  pool = pool.concat(flattenArtifact(arts))

  let layerPool = filterBySource(pool, layerName, source)

  if (!layerPool.length){
    const byFlag = pool.filter(i=>{
      const f = i['是否副本掉落']
      if (source==='副本') return f === true || lodash.isUndefined(f)
      return f === false || lodash.isUndefined(f)
    })
    layerPool = byFlag.length ? byFlag : pool
  }

  if (source==='副本' && dungeon['专属掉落']){
    layerPool = applyWhitelist(layerPool, dungeon['专属掉落'])
  }

  if (source==='秘境'){
    layerPool = filterByRange(layerPool, dungeon)
  }

  const byCat = lodash.groupBy(layerPool, x=>x.类别)
  // 只对有候选的类别进行权重分配
  const weightsAvail = {}
  for (const k of Object.keys(weights)){
    if ((byCat[k]||[]).length > 0){
      weightsAvail[k] = weights[k]
    }
  }
  const alloc = allocCounts(total, Object.keys(weightsAvail).length ? weightsAvail : weights)
  const result = []
  const rng = createPRNG(opts.seed)
  const seen = new Set()
  const unique = !!opts.unique
  const perLimit = opts.perCategoryLimit || {}
  const byCatCounts = {}
  for (const k of Object.keys(byCat)) byCatCounts[k] = byCat[k].length
  for (const cat of Object.keys(byCat)){
    const limit = Number.isFinite(perLimit[cat]) ? Math.max(0, perLimit[cat]) : (alloc[cat]||0)
    const cands = byCat[cat].map(i=>({i,w:weightOf(i, layerName, coeff, source)}))
    const picks = pickWeighted(cands, limit, rng)
    for (const p of picks){
      const key = `${cat}:${p.id || ''}:${p['部位'] || ''}:${p['套装名称'] || ''}`
      if (unique && seen.has(key)) continue
      seen.add(key)
      if (p.类别 === '圣遗物'){
        const enriched = generateArtifactFromBase(p)
        result.push({ ...enriched, 来源: layerName })
      } else {
        result.push({
          类别: p.类别,
          套装名称: p['套装名称'],
          元素: p['元素'],
          星级: p['星级'],
          稀有度: p['稀有度'],
          id: p.id,
          名称: p['名称'],
          部位: p['部位'],
          来源: layerName
        })
      }
      if (result.filter(x=>x.类别===cat).length >= limit) break
    }
  }
  // 兜底：若分配后仍为空或未达总数，按全池加权补足
  if (result.length < total){
    const remaining = total - result.length
    const allCands = layerPool.map(i=>({i,w:weightOf(i, layerName, coeff, source)}))
    const extra = pickWeighted(allCands, remaining, rng)
    for (const p of extra){
      const key = `${p.类别}:${p.id || ''}:${p['部位'] || ''}:${p['套装名称'] || ''}`
      if (unique && seen.has(key)) continue
      seen.add(key)
      if (p.类别 === '圣遗物'){
        const enriched = generateArtifactFromBase(p)
        result.push({ ...enriched, 来源: layerName })
      } else {
        result.push({
          类别: p.类别,
          套装名称: p['套装名称'],
          元素: p['元素'],
          星级: p['星级'],
          稀有度: p['稀有度'],
          id: p.id,
          名称: p['名称'],
          部位: p['部位'],
          来源: layerName
        })
      }
    }
  }
  const ret = {来源:source, 层级:layer, 掉落:result}
  if (opts.includeDebug){
    const nonZero = layerPool.map(i=>({
      名称: i['名称'], 类别: i['类别'], 部位: i['部位'], 星级: i['星级'], 稀有度: i['稀有度'], 套装名称: i['套装名称'],
      w: weightOf(i, layerName, coeff, source)
    })).filter(x=>x.w>0).sort((a,b)=>b.w-a.w).slice(0,5)
    const altLayerName = source==='副本' ? layerName.replace(/^副本/, '秘境') : layerName.replace(/^秘境/, '副本')
    const matchStats = {
      exact: layerPool.filter(i=>safeArray(i['掉落来源']).includes(layerName)).length,
      alt: layerPool.filter(i=>safeArray(i['掉落来源']).includes(altLayerName)).length
    }
    const nonZeroByCat = {}
    for (const k of Object.keys(byCat)){
      nonZeroByCat[k] = (byCat[k]||[]).map(i=>weightOf(i, layerName, coeff, source)).filter(w=>w>0).length
    }
    const probKeysSample = {}
    for (const k of Object.keys(byCat)){
      const arr = (byCat[k]||[]).slice(0,3)
      probKeysSample[k] = arr.map(i=>({ 名称:i['名称'], 套装名称:i['套装名称'], 部位:i['部位'], keys:Object.keys(i['概率']||{}) }))
    }
    const weightsAvail = {}
    for (const k of Object.keys(weights)){
      if ((byCat[k]||[]).length>0) weightsAvail[k] = weights[k]
    }
    const limits = {}
    for (const cat of Object.keys(byCat)){
      const limit = Number.isFinite(perLimit[cat]) ? Math.max(0, perLimit[cat]) : (alloc[cat]||0)
      limits[cat] = limit
    }
    ret.debug = {
      total,
      weights,
      weightsRaw: weightsRaw,
      weightsAvail,
      alloc,
      limits,
      layerName,
      candidateCounts: byCatCounts,
      nonZeroWeightCounts: nonZeroByCat,
      layerPoolCount: layerPool.length,
      matchBySource: matchStats,
      probKeysSample,
      picksCount: result.length,
      topCandidates: nonZero,
      dungeonInfo: {
        baseTimes,
        maxTimes,
        hasWhitelist: !!dungeon['专属掉落'],
        hasRange: source==='秘境'
      },
      dataLoadCounts: {
        equipSets: equipSets.length,
        elixirs: elixirs.length,
        materials: mats.length,
        artifacts: arts.length
      }
    }
  }
  return ret
}

async function dropAndLog({ uid, source='副本', layer=1, opts={}, lost=[], shouldGain=[], shouldLose=[], success=true, reason='', context={} }){
  const res = await drop(source, layer, opts)
  const gained = (res.掉落||[]).map(p=>({
    类别: p.类别,
    套装名称: p['套装名称'],
    元素: p['元素'],
    名称: p['名称'],
    部位: p['部位'],
    星级: p['星级'],
    稀有度: p['稀有度'],
    id: p.id,
    数量: 1
  }))
  const log = await ActionLog.log({ uid, action: `${source}掉落结算`, source, layer, gained, lost, shouldGain, shouldLose, success, reason, context })
  return { ...res, log }
}

export default { drop, dropAndLog }
